摘要
Day 19 已經把 Quality Test Report、規則覆蓋矩陣與契約版本比較整理成可閱讀證據。Day 20 回頭處理一個更根本的問題:交換契約規則不能看起來像寫死在程式裡的假規則,所以今天把規則啟用與 LOINC / UCUM 允許集合抽成可載入的合作方 contract 檔。
FHIR / TW Core validation 可以回答:
這份資料在標準資料結構與 Profile 下是否合法?
但資料交換還會遇到另一層問題:
合作方在這個交換情境下,要求哪些規則一定要檢查?
哪些 LOINC / UCUM 值是這次交換契約允許的?
所以 Day 20 的重點是把「合作方政策」從 rule implementation 裡拆出來。
因此變成:
Java rule classes = 規則引擎實作
contract JSON files = 合作方交換政策
Quality Gate = 用指定 contract 驗證同一份 Bundle 的結果
這樣比較接近真實醫療交換裡的 implementation guide、companion guide、trading partner agreement 或 interface specification 概念。
名稱不一定都叫 contract,但核心都是:標準之外,交換雙方仍會有場域或合作方特定要求。
今天新增或修改的範圍有:
src/main/resources/contracts/demo-lab-v1.0.json
src/main/resources/contracts/demo-lab-v1.1.json
src/main/java/com/twlab/qualitygate/validation/ExchangeContract.java
src/main/java/com/twlab/qualitygate/validation/ExchangeContractService.java
src/main/java/com/twlab/qualitygate/validation/BundleParseService.java
src/main/java/com/twlab/qualitygate/validation/ContractComparisonService.java
src/main/java/com/twlab/qualitygate/validation/ContractRule.java
src/main/java/com/twlab/qualitygate/validation/LabCode001ObservationLoincRule.java
src/main/java/com/twlab/qualitygate/validation/LabUnit002ObservationUcumCodeRule.java
src/main/java/com/twlab/qualitygate/web/ParseController.java
src/main/resources/templates/index.html
src/test/java/com/twlab/qualitygate/validation/*Tests.java
src/test/java/com/twlab/qualitygate/web/ParseControllerTests.java
README.md
Day 20 做五件事:
新增內建合作方 contract 檔。
讓規則啟用與允許值由 contract 驅動。
讓使用者可上傳合作方 contract JSON 影響本次驗證。
讓升版比較改成需要時才勾選執行,且比較使用者上傳的二至多個 contract 版本。
移除舊的 hardcoded ContractVersion enum。
Inferno、Touchstone、TestScript 主要用來測 FHIR Server / Client 的互通行為。
它們通常需要 endpoint、API interaction、search、read、authorization 或完整 test kit。
本專案目前的 MVP 輸入是一份貼上或上傳的 lab Bundle。
所以 Day 20 不把範圍擴成 server certification。
今天只先把一個概念落地:
交換政策不寫死在程式裡。
同一份 Bundle 可以用不同 contract 驗證。
contract 版本差異可以造成不同 Quality Gate 結果。
這也讓本專案和通用 FHIR validator 有比較明確的分工:
validator.fhir.org / HAPI validator 負責回答「Resource 是否符合 FHIR / Profile」。
Inferno / Touchstone 負責回答「FHIR API / Server 行為是否符合測試情境」。
本專案這一層則聚焦在「資料送出前,是否符合這次合作方交換契約」,並把不符合的地方轉成可修正的 blocking evidence。
Day 20 新增兩份內建 contract:
src/main/resources/contracts/demo-lab-v1.0.json
src/main/resources/contracts/demo-lab-v1.1.json
v1.1 的內容重點如下:
{
"id": "demo-lab-hospital-a",
"name": "Demo Lab to Hospital A Exchange Contract",
"version": "1.1",
"enabledRuleCodes": [
"LAB-REF-001",
"LAB-REF-002",
"LAB-REF-003",
"LAB-CODE-001",
"LAB-UNIT-001",
"LAB-UNIT-002"
],
"allowedLoincCodes": ["2345-7", "718-7"],
"allowedUcumCodes": ["mg/dL", "mmol/L"]
}
v1.0 和 v1.1 最大差異是:
| Contract | 啟用規則 |
|---|---|
demo-lab-hospital-a#1.0 |
LAB-REF-001、LAB-REF-002、LAB-REF-003、LAB-CODE-001、LAB-UNIT-001 |
demo-lab-hospital-a#1.1 |
v1.0 全部規則,再加上 LAB-UNIT-002 |
也就是說,LAB-UNIT-002 不再是 ContractVersion enum 寫死的版本差異。
它現在來自 v1.1 contract file 的 enabledRuleCodes。
今天把原本混在一起的兩件事拆開。
Java rule class 仍然負責:
Observation、DiagnosticReport、Patient。Observation.code.coding 是否含有 LOINC。valueQuantity.system/code 是否符合 UCUM policy。RuleResult。Contract file 則負責:
這個分工讓 LAB-CODE-001 和 LAB-UNIT-002 的意義更清楚:
| 規則 | Java 負責 | Contract 負責 |
|---|---|---|
LAB-CODE-001 |
檢查 Observation.code.coding 是否有 http://loinc.org |
決定允許 2345-7、718-7 |
LAB-UNIT-002 |
檢查 valueQuantity.system/code |
決定允許 mg/dL、mmol/L |
所以現在不再是:
程式寫死只允許 mg/dL 或 mmol/L。
而是:
目前載入的 demo-lab-hospital-a#1.1 contract 允許 mg/dL 或 mmol/L。
Day 19 的版本比較是靠 ContractVersion enum:
V1_0 -> 啟用五條規則
V1_1 -> 啟用六條規則
這可以跑,但 policy source of truth 在 Java code 裡。
Day 20 移除這個 enum。
現在版本比較改成兩層:
demo-lab-v1.0.json / demo-lab-v1.1.json -> 保留為可重現的 demo contract
使用者上傳二至多個 contract version JSON -> UI 實際執行 comparison
ContractComparisonService 不再問 enum 哪些規則啟用。
它改成接收指定的 contract list,對同一份 Bundle 逐一驗證。
首頁的 Compare contract versions 不再自動拿內建 v1.0 / v1.1 比較;使用者必須上傳二至多個 contract version JSON。
這讓「契約版本比較」更接近真實情境:
同一份資料,在舊合作方契約下能通過。
同一份資料,在新合作方契約下被新增規則阻擋。
Day 20 也把 contract metadata 顯示到首頁。
首頁一打開就會看到 Loaded partner contracts,列出目前預設驗證 contract 與 comparison upload 入口:
Default validation: demo-lab-hospital-a#1.1
Current validation: demo-lab-hospital-a#1.1
Comparison upload: Upload two or more contract version JSON files
Contract upload: Optional JSON upload for the current validation only
送出驗證後,Quality Test Report 的 Input summary 也會顯示:
Exchange contract: demo-lab-hospital-a#1.1
Contract version comparison 表格也新增:
Loaded contract
所以使用者不是只看到抽象的 v1.0 / v1.1。
他可以看到實際載入的 contract id / version。
預設 contract 仍然來自 application resources 裡的 bundled contract。
如果使用者上傳合作方 contract JSON,該 contract 只影響本次 Current validation。Contract version comparison 改成需要比較版本時才勾選執行;執行時必須由使用者上傳二至多個 contract version JSON。
第一個上傳的 contract 會作為 baseline,後續版本會和 baseline 比出新增的 failed rules。

Day 20 也把「載入合作方要求」做成可操作的 upload workflow。
Input Bundle 表單現在除了 Upload Bundle JSON,也多了:
Optional partner contract JSON
上傳的 contract JSON 會經過最小檢查:
ExchangeContract。id、name、version。enabledRuleCodes 不可為空。enabledRuleCodes 只能引用目前系統真的有實作的 rule code。目前這個 upload 和 comparison 是刻意限制範圍的 MVP:
uploaded contract -> 只影響本次 Current validation
Compare contract versions -> 勾選後才讀取使用者上傳的二至多個 contract version JSON
這樣可以同時做到兩件事:
第一,合作方可提供自己的允許 LOINC / UCUM 集合來驗證本次 Bundle。
第二,升版比較只在使用者真的提供版本檔時執行,不會拿內建版本假設合作方契約。

用 observation-quantity-wrong-ucum-system.json 驗證、勾選 Compare contract versions,並上傳 demo-lab-v1.0.json / demo-lab-v1.1.json 兩份 contract 時,結果仍然維持 Day 19 的示範效果:
v1.0 -> 沒有啟用 LAB-UNIT-002
v1.1 -> 啟用 LAB-UNIT-002 且失敗
但 Day 20 後,這個差異來源變成使用者上傳的 contract file:
| Contract | Gate | Failed rule |
|---|---|---|
demo-lab-hospital-a#1.0 |
PASSED |
None |
demo-lab-hospital-a#1.1 |
BLOCKED |
LAB-UNIT-002 |
Upgrade blocker evidence 仍然列出可修正證據:
Path: Observation/obs-wrong-ucum-system.valueQuantity.system/code
Actual: http://example.org/local-units|mg/dL
Expected: Observation.valueQuantity.system must be http://unitsofmeasure.org and code must be allowed by the exchange contract.
這次要注意一個細節:Expected 裡的 allowed value 不再是 rule class 的固定常數。suggestion 會依 contract 的 allowedUcumCodes 組出目前允許值。

本機 Maven 測試:
./mvnw test
結果:
Tests run: 65, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Day 20 新增或調整的測試確認:
ExchangeContractServiceTests 可以載入 v1.0 / v1.1 contract。LAB-UNIT-002。LAB-UNIT-002。ContractComparisonServiceTests 保持同一份 UCUM 錯誤資料在 v1.0 / v1.1 下結果不同。ContractScenarioCaseTests 的四組代表情境維持通過。Contract version comparison。Compare contract versions 且上傳兩份 contract version JSON 才會顯示升版比較。Compare contract versions 但未上傳至少兩份 contract 時會顯示錯誤。ExchangeContract model。ExchangeContractService 從 classpath 載入內建 contract。demo-lab-v1.0.json。demo-lab-v1.1.json。ContractVersion enum。BundleParseService 改用 ExchangeContract 執行驗證。ContractComparisonService 改成可接收指定 contract list 做多版本比較。ContractRule 介面改成接收 ExchangeContract。LAB-CODE-001 改讀 allowedLoincCodes。LAB-UNIT-002 改讀 allowedUcumCodes。Compare contract versions checkbox。./mvnw test 通過,測試數 65。Day 20 尚未處理:
COMPATIBLE / EXPECTED_BREAKING_CHANGE / UNEXPECTED_REGRESSION 三分類。目前的 MVP 進度:
validation-flow
├─ JSON parse 完成
├─ FHIR R4 parse 完成
├─ FHIR R4 validation 完成
├─ TW Core validation / safe NOT_EVALUATED 完成
├─ Partner exchange contract loading
│ ├─ demo-lab-v1.0.json 完成
│ ├─ demo-lab-v1.1.json 完成
│ ├─ enabledRuleCodes 完成
│ ├─ allowedLoincCodes 完成
│ ├─ allowedUcumCodes 完成
│ └─ optional uploaded contract 完成最小版
├─ Exchange contract rules
│ ├─ LAB-REF-001 完成並由 contract 啟用
│ ├─ LAB-REF-002 完成並由 contract 啟用
│ ├─ LAB-REF-003 完成並由 contract 啟用
│ ├─ LAB-CODE-001 完成,允許值由 contract 提供
│ ├─ LAB-UNIT-001 完成並由 contract 啟用
│ └─ LAB-UNIT-002 完成,允許值由 contract 提供
├─ Quality Gate 完成最小版
├─ Contract comparison
│ ├─ v1.0 / v1.1 contract file loading 完成
│ ├─ comparison service test 完成
│ ├─ homepage comparison display 完成
│ ├─ compare checkbox 完成
│ ├─ uploaded 2+ version comparison 完成
│ └─ upgrade blocker evidence display 完成
├─ Scenario test pack
│ ├─ v1.0 / v1.1 representative cases 完成 4 例
│ └─ NOT_APPLICABLE scenario fixture 完成 1 例
├─ Homepage Quality Test Report 完成最小版
└─ Reproducible delivery
├─ Dockerfile 完成最小版
├─ Docker Compose 完成最小版並驗證啟動
└─ GitHub Actions CI 完成最小版
下一步預計處理:
Contract JSON Schema + contract diff report / export
Repository:twcore-data-quality-gate